/**
 *  Copyright 2011 Pavlov Dm.
 *  Licensed under the MIT License
 */

package org.motiv.store;

import org.motiv.core.Element;
import org.motiv.core.CacheException;
import org.motiv.policy.*;
import org.motiv.core.Cache;

import java.util.concurrent.ConcurrentHashMap;
import java.util.Iterator;
import java.util.NoSuchElementException;
import org.apache.log4j.Logger;

/**
 * A Store implementation class. 
 * @author Pavlov Dm
 */
public class MemoryStore implements Store {  
	/** Logger instance */
	private static final Logger LOG=Logger.getLogger(MemoryStore.class);
	/**
     * The maximum size of the store (0 == no limit)
     */
    protected volatile int maximumSize;
	/**
    * The cache this store is associated with.
    */
    protected Cache cache;
	/**
     * The DiskStore associated with this MemoryStore.
     */
    protected final Store diskStore;
	
	private static final int MAX_EVICTION_RATIO = 5;
	/**
     * Map where items are stored by key.
     */
	private volatile ConcurrentHashMap map = new ConcurrentHashMap();
	 /**
     * The eviction policy to use
     */
    protected volatile Policy policy;
    /**
     * Constructs things that all MemoryStores have in common.
     */
    public MemoryStore(final Cache cache, Store diskStore) {
        this.cache = cache;
        this.maximumSize = cache.getCacheConfiguration().getMaxElementsInMemory();
        this.diskStore = diskStore;
		policy = determineEvictionPolicy();
    }
    /**
     * Puts an item in the store.
     * @param element the element to add
     */
    public final boolean put(final Element element) throws CacheException {
        boolean newPut = true;
        if (element != null) {
            newPut = map.put(element.getObjectKey(), element) == null;
            checkForEviction(element);
        }
        return newPut;
    }
    /**
     * Gets an item from the cache.
     * @param key the key of the Element
     * @return the element, or null if there was no match for the key
     */
    public final Element get(final Object key) {
		if (key == null) return null;
		
		Element element = (Element) map.get(key);
		
		if (diskStore != null && element==null) {
			if(diskStore.containsKey(key)){
				element = diskStore.get(key);
			}
		}
        return element;
    }
    /**
     * Removes an Element from the store.
     * @param key the key of the Element, usually a String
     * @return the Element if one was found, else null
     */
    public final Element remove(final Object key) {
        if (key == null) return null;
        // remove element item.
        Element element = (Element) map.remove(key);
		
		if (diskStore != null && element==null) {
			if(diskStore.containsKey(key)){
				element = diskStore.remove(key);
			}
		}
        return element;
    }  
    /**
     * Remove all of the elements from the store.
     */
    public final void removeAll() throws CacheException {

		map.clear();
		
		if (diskStore != null) {
			diskStore.removeAll();
		}
    }
    /**
     * Prepares for shutdown.
     */
    public final synchronized void dispose() {
		map.clear();
        //release reference
        map = null;
		cache = null;
		if (diskStore != null) {
			diskStore.dispose();
		}
    }  
	 /**
     * Check element for eviction method 
	 * @param elementJustAdded
     */
    protected void checkForEviction(final Element elementJustAdded) {
        if (maximumSize > 0) {
            int evict = Math.min(map.size() - maximumSize, MAX_EVICTION_RATIO);
            for (int i = 0; i < evict; i++) {
                removeElementChosenByEvictionPolicy(elementJustAdded);
            }
        }
    }
	/**
     * Removes the element chosen by the eviction policy
     * @param elementJustAdded it is possible for this to be null
     */
    protected void removeElementChosenByEvictionPolicy(final Element elementJustAdded) {
		if(LOG.isDebugEnabled()){
			LOG.debug("Cache is full. Removing element ...");
		}
        Element element = findEvictionCandidate(elementJustAdded);
        if (element == null) {
            LOG.debug("Eviction selection miss. Selected element is null");
            return;
        }
        // If the element is expired, remove
        if (element.isExpired()) {
            remove(element.getObjectKey());
            return;
        }
        evict(element);
        remove(element.getObjectKey());
    }
	
	/**
     * Find a "relatively" unused element, but not the element just added.
     */
    protected final Element findEvictionCandidate(final Element elementJustAdded) {
		Element[] elements = sampleElements(map.size());
		return policy.selectedBasedOnPolicy(elements, elementJustAdded);
    }
	/**
    * Uses random numbers to sample the entire map.
    * @return a random sample of elements
    */
    protected Element[] sampleElements(final int size) {
        int[] offsets = BasePolicy.generateRandomSample(size);
        Element[] elements = new Element[offsets.length];
        Iterator iterator = map.values().iterator();
        for (int i = 0; i < offsets.length; i++) {
            for (int j = 0; j < offsets[i]; j++) {
                //fast forward
                try {
                    iterator.next();
                } catch (NoSuchElementException e) {
                    //e.printStackTrace();
                }
            }

            try {
                elements[i] = ((Element) iterator.next());
            } catch (NoSuchElementException e) {
                //e.printStackTrace();
            }
        }
        return elements;
    }
	/**
     * Evict the <code>Element</code>.
     * @param element the <code>Element</code> to be evicted.
     */
    protected final void evict(final Element element) throws CacheException {
       spoolToDisk(element);
    }
	/**
     * Puts the element in the DiskStore.
     * Should only be called if overflowToDisk is true
     * <p/>
     * Relies on being called from a synchronized method
     *
     * @param element The Element
     */
    protected void spoolToDisk(final Element element) {
        if (diskStore != null) {
            diskStore.put(element);
            if (LOG.isDebugEnabled()) {
                LOG.debug(cache.getName() + "Cache: spool to disk done for: " + element.getObjectKey());
            }
        }
    }
	/**
     * Chooses the Policy from the cache configuration
     */
    protected final Policy determineEvictionPolicy() {
        MemoryStoreEvictionPolicy policySelection = cache.getCacheConfiguration().getMemoryStoreEvictionPolicy();

        if (policySelection.equals(MemoryStoreEvictionPolicy.LRU)) {
            return new LruPolicy();
        } else if (policySelection.equals(MemoryStoreEvictionPolicy.FIFO)) {
            return new FifoPolicy();
        } else if (policySelection.equals(MemoryStoreEvictionPolicy.LFU)) {
            return new LfuPolicy();
        }

        throw new IllegalArgumentException(policySelection + " isn't a valid eviction policy");
    }
	public boolean containsKey(Object key) {
        return map.containsKey(key);
    }

}

